package ar.edu.itba.it.cg.grupo01.impl;

import static com.google.common.base.Preconditions.checkArgument;

import java.util.Iterator;

import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

import ar.edu.itba.it.cg.grupo01.api.CameraInterface;
import ar.edu.itba.it.cg.grupo01.api.Ray;

public class Camera implements CameraInterface {

	private Point3d origin;
	private Vector3d viewingDirection;
	private Vector3d upVector;
	private int xResolution;
	private int yResolution;
	private double fov;
	private Vector3d normal;
	private Point3d initialPoint;
	private Iterator<CameraRay> rayIterator;

	public Camera(Point3d origin, Vector3d viewingDirection, Vector3d upVector,
			int resX, int resY, double fov) {
		
		initializeCameraParameters(origin, viewingDirection, upVector, resX, resY, fov);
		
		// TODO remember jabu
		// El archivo de escena viene mal entonces hay que hacer up . viewing
		// para sacar el normal y despues normal . viewing para tener el up
		// arreglado
		
		this.normal = (Vector3d) viewingDirection.clone();
		normal.cross(normal, upVector);
		normal.normalize();
		
		Point3d focalPoint = (Point3d) origin.clone();
		
		double distance = calculateFocalPlaneDistance();
		
		Vector3d p = (Vector3d) viewingDirection.clone();
		
		p.scale(distance);
		
		focalPoint.add(p);

		Vector3d u = new Vector3d(normal.x, normal.y, normal.z);
		Vector3d v = new Vector3d(this.upVector.x, this.upVector.y, this.upVector.z);

		u.scale(xResolution / 2);
		v.scale(yResolution / 2);
		
		focalPoint.add(u);
		focalPoint.add(v);

		this.initialPoint = new Point3d(focalPoint.x, focalPoint.y,
				focalPoint.z);
		
		this.rayIterator = new SingleRay(this);
	}


	@Override
	public int getxResolution() {
		return xResolution;
	}

	@Override
	public int getyResolution() {
		return yResolution;
	}

	@Override
	public Ray getRay(int x, int y) {

		validateRayParameters(x, y);

		Vector3d u = (Vector3d) normal.clone();
		Vector3d v = (Vector3d) upVector.clone();

		//Vectors u & v must be inverted, so, multiply by -1
		scaleVector(v, x*(-1));
		scaleVector(u, y*(-1));
		
		Vector3d answer = constructResultVector(u, v);
		
		Ray ray = constructRay(answer);
		
		return ray;
	}

	@Override
	public Iterator<CameraRay> iterator() {
		return rayIterator;
	}

	@Override
	public void setIterator(Iterator<CameraRay> iterator) {
		this.rayIterator = iterator;
	}

	@Override
	public Point3d getOrigin() {
	return (Point3d) origin.clone();
	}
	
	//Private Members
	private void initializeCameraParameters(Point3d origin, Vector3d viewingDirection,
			Vector3d upVector, int resX, int resY, double fov) {
		this.origin = (Point3d) origin.clone();
		this.viewingDirection = (Vector3d) viewingDirection.clone();
		this.viewingDirection.normalize();
		this.upVector = (Vector3d) upVector.clone();
		this.upVector.normalize();
		this.yResolution = resY;
		this.xResolution = resX;
		this.fov = fov * 2 * Math.PI / 360;
	}
	
	//Constructs a Ray from a given direction
	private Ray constructRay(Vector3d answer) {
		Ray ray = new RayImpl(origin, answer);
		return ray;
	}
	
	//Scales a vector
	private void scaleVector(Vector3d aVector, int scalarValue){
		aVector.scale(scalarValue);
	}

	//Constructs a resulting vector from the addition of a given pair
	private Vector3d constructResultVector(Vector3d u, Vector3d v) {
		Vector3d answer = new Vector3d(initialPoint.x, initialPoint.y, initialPoint.z);
		answer.add(u);
		answer.add(v);
		return answer;
	}	

	private void validateRayParameters(int x, int y) {
		checkArgument(x >= 0 && x < yResolution,
				"x should be greater than 0 and lower than " + xResolution
				+ ". Got: " + x);
		checkArgument(y >= 0 && y < xResolution,
				"y should be greater than 0 and lower than " + yResolution
				+ ". Got: " + y);
	}
	
	private double calculateFocalPlaneDistance() {
		double distance = (xResolution / 2.0) / Math.tan(this.fov / 2);
		return distance;
	}

}
